/**
 * IncludedResourceContentMap.java
 *
 * Copyright 2011 Jack Leow
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package my.edu.clhs.stringtemplate.servlet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * {@link java.util.Map} that maps Servlet resource URIs to their contents for
 * inclusion in a StringTemplate page.
 *
 * This map is immutable. It also does not allow iteration over keys/values.
 * More specifically, only the {@link #containsKey(Object)} and
 * {@link #get(Object)} methods are implemented. All other {@link java.util.Map}
 * methods throw <tt>UnsupportedOperationException</tt>.
 *
 * @author Jack Leow
 * @version 1.0
 * @since 1.0 (Jan 22, 2011, 7:13:42 PM)
 */
public class IncludedResourceContentMap extends AbstractMap {
    private final HttpServletRequest request;
    private final HttpServletResponse response;
    
    public IncludedResourceContentMap(
            HttpServletRequest request, HttpServletResponse response) {
        if (request == null) {
            throw new NullPointerException("request must be non-null");
        }
        if (response == null) {
            throw new NullPointerException("response must be non-null");
        }
        this.request = request;
        this.response = response;
    }
    
    /**
     * HttpServletRequest that wraps an existing HttpServletRequest but isolates
     * state changes from the wrapped request.
     */
    private static class IsolatedHttpServletRequest
            extends HttpServletRequestWrapper {
        private Map isolatedAttributes = new HashMap();
        private List removedAttributeNames = new LinkedList();
        
        /**
         * Creates an IsolatedHttpServletRequest that wraps an existing request.
         * 
         * @see javax.servlet.http.HttpServletRequestWrapper#
         *      HttpServletRequestWrapper(HttpServletRequest)
         */
        public IsolatedHttpServletRequest(HttpServletRequest request) {
            super(request);
        }
        
        public Object getAttribute(String name) {
            Object attrVal = isolatedAttributes.get(name);
            
            if (attrVal == null && !removedAttributeNames.contains(name)) {
                attrVal = super.getAttribute(name);
            }
            
            return attrVal;
        }
        
        public Enumeration getAttributeNames() {
            return new Enumeration() {
                private Iterator isolatedAttrNamesIter =
                    isolatedAttributes.keySet().iterator();
                private Iterator wrappedAttrNamesIter;
                
                {
                    ArrayList wrappedAttrNames = Collections.list(
                        IsolatedHttpServletRequest.super.getAttributeNames());
                    
                    wrappedAttrNames.removeAll(removedAttributeNames);
                    wrappedAttrNamesIter = wrappedAttrNames.iterator();
                }
                
                public boolean hasMoreElements() {
                    return isolatedAttrNamesIter.hasNext() ||
                        wrappedAttrNamesIter.hasNext();
                }
                
                public Object nextElement() {
                    Object element;
                    
                    if (isolatedAttrNamesIter.hasNext()) {
                        element = isolatedAttrNamesIter.next();
                    } else if (wrappedAttrNamesIter.hasNext()) {
                        element = wrappedAttrNamesIter.next();
                    } else {
                        throw new NoSuchElementException();
                    }
                    
                    return element;
                }
            };
        }
        
        public void removeAttribute(String name) {
            isolatedAttributes.remove(name);
            removedAttributeNames.add(name);
        }
        
        public void setAttribute(String name, Object o) {
            if (name == null) throw new NullPointerException();
            if (o == null) {
                removeAttribute(name);
            } else {
                isolatedAttributes.put(name, o);
                removedAttributeNames.add(name);
            }
        }
        
        public void setCharacterEncoding(String enc)
                throws UnsupportedEncodingException {
            // do nothing
        }
    }
    
    /**
     * HttpServletResponse that captures the contents written into a buffer 
     * instead of to the client.
     */
    private static class ContentCapturingHttpServletResponse
            extends HttpServletResponseWrapper {
        // Platform default line separator
        private static final String LINESEP =
            System.getProperty("line.separator");
        // Servlet specification default character set
        private static final String DEFAULT_CHARSET = "ISO-8859-1";
        private StringBuffer contentBuffer = new StringBuffer();
        private ServletOutputStream outputStream = null;
        private PrintWriter writer = null;
        
        /**
         * Creates a ContentCapturingHttpServletResponse that wraps an existing
         * response.
         * 
         * @see javax.servlet.http.HttpServletResponseWrapper#
         *      HttpServletResponseWrapper(HttpServletResponse)
         */
        public ContentCapturingHttpServletResponse(
                HttpServletResponse response) {
            super(response);
        }
        
        public void flushBuffer() throws IOException {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void addCookie(Cookie cookie) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void addDateHeader(String name, long date) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void addHeader(String name, String value) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void addIntHeader(String name, int value) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void sendError(int sc, String msg) throws IOException {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void sendError(int sc) throws IOException {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void sendRedirect(String location) throws IOException {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setDateHeader(String name, long date) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setHeader(String name, String value) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setIntHeader(String name, int value) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setStatus(int sc, String sm) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setStatus(int sc) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setContentLength(int len) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setContentType(String type) {
            // do nothing, don't change the state of the wrapped response
        }
        
        public void setLocale(Locale loc) {
            // do nothing, don't change the state of the wrapped response
        }
        
        /**
         * Returns content captured by this HttpServletResponse.
         * 
         * @return captured content.
         */
        public String getContent() {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                } catch (IOException e) {
                    // this should never happen in our implementation
                    throw new RuntimeException(e);
                }
            }
            
            return contentBuffer.toString();
        }
        
        public ServletOutputStream getOutputStream() throws IOException {
            // The Servlet container should allow only one thread in at a time, so
            // we shouldn't have to worry about concurrency here :S. -Jack Leow
            if (writer != null) {
                throw new IllegalStateException();
            }
            if (outputStream == null) {
                outputStream = new ServletOutputStream() {
                    private ByteArrayOutputStream buffer =
                        new ByteArrayOutputStream();
                    
                    public void print(boolean b) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(b);
                    }
                    
                    public void print(char c) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(c);
                    }
                    
                    public void print(double d) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(d);
                    }
                    
                    public void print(float f) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(f);
                    }
                    
                    public void print(int i) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(i);
                    }
                    
                    public void print(long l) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(l);
                    }
                    
                    public void print(String s) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(s);
                    }
                    
                    public void println() throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(LINESEP);
                    }
                    
                    public void println(boolean b) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(b).append(LINESEP);
                    }
                    
                    public void println(char c) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(c).append(LINESEP);
                    }
                    
                    public void println(double d) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(d).append(LINESEP);
                    }
                    
                    public void println(float f) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(f).append(LINESEP);
                    }
                    
                    public void println(int i) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(i).append(LINESEP);
                    }
                    
                    public void println(long l) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(l).append(LINESEP);
                    }
                    
                    public void println(String s) throws IOException {
                        if (buffer.size() > 0) {
                            flush();
                        }
                        contentBuffer.append(s).append(LINESEP);
                    }
                    
                    public void write(int b) throws IOException {
                        buffer.write(b);
                    }
                    
                    public void flush() throws UnsupportedEncodingException {
                        try {
                            contentBuffer.append(new String(
                                buffer.toByteArray(), getCharacterEncoding()));
                        } catch (UnsupportedEncodingException e) {
                            // try the Servlet spec default
                            contentBuffer.append(new String(
                                buffer.toByteArray(), DEFAULT_CHARSET));
                        }
                        buffer.reset();
                    }
                };
            }
            
            return outputStream;
        }
        
        public PrintWriter getWriter() throws IOException {
            // The Servlet container should allow only one thread in at a time, so
            // we shouldn't have to worry about concurrency here :S. -Jack Leow
            if (outputStream != null) {
                throw new IllegalStateException();
            }
            if (writer == null) {
                writer = new PrintWriter(new ByteArrayOutputStream()) {
                    public void print(boolean b) {
                        contentBuffer.append(b);
                    }
                    
                    public void print(char c) {
                        contentBuffer.append(c);
                    }
                    
                    public void print(char[] s) {
                        contentBuffer.append(s);
                    }
                    
                    public void print(double d) {
                        contentBuffer.append(d);
                    }
                    
                    public void print(float f) {
                        contentBuffer.append(f);
                    }
                    
                    public void print(int i) {
                        contentBuffer.append(i);
                    }
                    
                    public void print(long l) {
                        contentBuffer.append(l);
                    }
                    
                    public void print(Object obj) {
                        contentBuffer.append(obj);
                    }
                    
                    public void print(String s) {
                        contentBuffer.append(s);
                    }
                    
                    public void println() {
                        contentBuffer.append(LINESEP);
                    }
                    
                    public void println(boolean x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(char x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(char[] x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(double x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(float x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(int x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(long x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(Object x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void println(String x) {
                        contentBuffer.append(x).append(LINESEP);
                    }
                    
                    public void write(char[] buf, int off, int len) {
                        contentBuffer.append(buf, off, len);
                    }
                    
                    public void write(char[] buf) {
                        contentBuffer.append(buf);
                    }
                    
                    public void write(int c) {
                        contentBuffer.append((char)c);
                    }
                    
                    public void write(String s, int off, int len) {
                        contentBuffer.append(s.toCharArray(), off, len);
                    }
                    
                    public void write(String s) {
                        contentBuffer.append(s);
                    }
                };
            }
            
            return writer;
        }
    }
    
    public Set entrySet() {
        throw new UnsupportedOperationException("not implemented");
    }
    
    public boolean containsKey(Object key) {
        return request.getRequestDispatcher(key.toString()) != null;
    }
    
    public Object get(Object key) {
        String content = null;
        
        RequestDispatcher requestDispatcher =
            request.getRequestDispatcher(key.toString());
        if (requestDispatcher != null) {
            HttpServletRequest req = new IsolatedHttpServletRequest(request);
            ContentCapturingHttpServletResponse resp =
                new ContentCapturingHttpServletResponse(response);
            
            try {
                requestDispatcher.include(req, resp);
                content = resp.getContent();
            } catch (ServletException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        return content;
    }
}